package com.transcasafe.eanpay.core.http;


import com.transcasafe.eanpay.core.auth.Credential;
import com.transcasafe.eanpay.core.auth.Validator;
import com.transcasafe.eanpay.core.exception.MalformedMessageException;
import com.transcasafe.eanpay.core.exception.PayApiException;
import com.transcasafe.eanpay.core.exception.ServiceException;
import com.transcasafe.eanpay.core.exception.ValidationException;
import com.transcasafe.eanpay.core.util.NonceUtil;
import com.transcasafe.eanpay.core.util.ShaUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.Objects.requireNonNull;

@Slf4j
public abstract class AbstractHttpClient implements HttpClient {
    private static final int NONCE_LENGTH = 32;
    protected final Validator validator;
    protected final Credential credential;
    private final okhttp3.OkHttpClient okHttpClient;

    public AbstractHttpClient(Validator validator, Credential credential, OkHttpClient okHttpClient) {
        this.validator = requireNonNull(validator);
        this.credential = requireNonNull(credential);
        this.okHttpClient = okHttpClient;
    }

    @Override
    public <T> HttpResponse<T> execute(HttpRequest httpRequest, Class<T> responseClass) {

        HttpRequest innerRequest =
                new HttpRequest.Builder()
                        .url(httpRequest.getUrl())
                        .httpMethod(httpRequest.getHttpMethod())
                        .headers(httpRequest.getHeaders())
                        .addHeader(PayConstant.HTTP_HEADER_CONTENT_LANGUAGE, "zh-CN") // 语言为简体中文
                        .addHeader(PayConstant.HTTP_HEADER_CONTENT_TYPE, MediaType.APPLICATION_JSON.toString())
                        .addHeader(PayConstant.HTTP_HEADER_MCH_NO, credential.getMchNo())
                        .addHeader(PayConstant.HTTP_HEADER_NONCE, NonceUtil.createNonce(NONCE_LENGTH))
                        .addHeader(PayConstant.HTTP_HEADER_TIMESTAMP, String.valueOf(Instant.now().getEpochSecond()))
                        .addHeader(PayConstant.HTTP_HEADER_SIGNATURE, this.buildSignature(httpRequest.getHeaders(), httpRequest.getBody()))
                        .body(httpRequest.getBody())
                        .build();

        OriginalResponse originalResponse = innerExecute(innerRequest);
        validateResponse(originalResponse);
        return assembleHttpResponse(originalResponse, responseClass);
    }

    /**
     * 构建signature头
     *
     * @param headers
     * @param body
     * @return
     */
    private String buildSignature(HttpHeaders headers, RequestBody body) {
        StringBuilder sb = new StringBuilder();
        sb.append("algorithm=");
        sb.append(ShaUtil.SHA256);
        sb.append(",");
        sb.append("version=v1");
        sb.append(",");
        sb.append("signature=");
        sb.append(credential.getAuthorization(headers, this.getSignBody(body)));
        return sb.toString();
    }

    private OriginalResponse innerExecute(HttpRequest httpRequest) {
        Request okHttpRequest = buildOkHttpRequest(httpRequest);
        try (Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
            return assembleOriginalResponse(httpRequest, okHttpResponse);
        } catch (IOException e) {
            log.error("http request error,url={}, message={}", httpRequest.getUrl(), e.getMessage(), e);
            throw new PayApiException("请求外部接口异常");
        }
    }

    private Request buildOkHttpRequest(HttpRequest httpRequest) {
        Request.Builder okHttpRequestBuilder = new Request.Builder().url(httpRequest.getUrl());
        Map<String, String> headers = httpRequest.getHeaders().getHeaders();
        log.info("Request Headers: {}", headers);
        headers.forEach(okHttpRequestBuilder::addHeader);

        String method = httpRequest.getHttpMethod().name();
        okhttp3.RequestBody okHttpRequestBody =
                method.equals("GET") ? null : buildOkHttpRequestBody(httpRequest.getBody());
        // 打印请求体
        if (httpRequest.getBody() != null) {
            if (httpRequest.getBody() instanceof JsonRequestBody) {
                log.info("Request Body: {}", ((JsonRequestBody) httpRequest.getBody()).getBody());
            }
        }
        okHttpRequestBuilder.method(method, okHttpRequestBody);
        return okHttpRequestBuilder.build();
    }

    private okhttp3.RequestBody buildOkHttpRequestBody(
            RequestBody payRequestBody) {
        if (payRequestBody == null) {
            // create an empty request body
            return okhttp3.RequestBody.create("", null);
        }
        if (payRequestBody instanceof JsonRequestBody) {
            return createOkHttpRequestBody(payRequestBody);
        }

        log.error(
                "When an http request is sent and the okhttp request body is constructed, the requestBody parameter"
                        + " type cannot be found,requestBody class name[{}]",
                payRequestBody.getClass().getName());
        return null;
    }

    private okhttp3.RequestBody createOkHttpRequestBody(RequestBody payRequestBody) {
        return createRequestBody(
                ((JsonRequestBody) payRequestBody).getBody(),
                okhttp3.MediaType.parse(payRequestBody.getContentType()));
    }

    private okhttp3.RequestBody createRequestBody(String content, okhttp3.MediaType mediaType) {
        // use an OkHttp3.x compatible method
        return okhttp3.RequestBody.create(mediaType, content);
    }

    private void validateResponse(OriginalResponse originalResponse) {

        if (isInvalidHttpCode(originalResponse.getStatusCode())) {
            throw new ServiceException(
                    originalResponse.getRequest(),
                    originalResponse.getStatusCode(),
                    originalResponse.getBody());
        }

        if (originalResponse.getBody() != null
                && !originalResponse.getBody().isEmpty()
                && !MediaType.APPLICATION_JSON.equalsWith(originalResponse.getContentType())) {
            throw new MalformedMessageException(
                    String.format(
                            "Unsupported content-type[%s]%nhttpRequest[%s]",
                            originalResponse.getContentType(), originalResponse.getRequest()));
        }

        if (!validator.validate(originalResponse.getHeaders(), originalResponse.getBody())) {
            throw new ValidationException(
                    String.format(
                            "Validate response failed,the signature is incorrect.%n"
                                    + "responseHeader[%s]\tresponseBody[%.1024s]",
                            originalResponse.getHeaders(), originalResponse.getBody()));
        }
    }

    protected boolean isInvalidHttpCode(int httpCode) {
        return httpCode < HTTP_OK || httpCode >= HTTP_MULT_CHOICE;
    }

    private <T> HttpResponse<T> assembleHttpResponse(
            OriginalResponse originalResponse, Class<T> responseClass) {
        return new HttpResponse.Builder<T>()
                .originalResponse(originalResponse)
                .serviceResponseType(responseClass)
                .build();
    }

    private String getSignBody(RequestBody requestBody) {
        if (requestBody == null) {
            return "";
        }
        if (requestBody instanceof JsonRequestBody) {
            return ((JsonRequestBody) requestBody).getBody();
        }
        throw new UnsupportedOperationException(
                String.format("Unsupported RequestBody Type[%s]", requestBody.getClass().getName()));
    }

    private OriginalResponse assembleOriginalResponse(
            HttpRequest httpRequest, Response okHttpResponse) {
        Map<String, String> responseHeaders = assembleResponseHeader(okHttpResponse);
        try {
            return new OriginalResponse.Builder()
                    .request(httpRequest)
                    .headers(responseHeaders)
                    .statusCode(okHttpResponse.code())
                    .contentType(okHttpResponse.body() == null
                            || okHttpResponse.body().contentType() == null
                            ? null : Objects.requireNonNull(okHttpResponse.body().contentType()).toString())
                    .body(okHttpResponse.body().string())
                    .build();
        } catch (IOException e) {
            log.error("OriginalResponse build error,message={}", e.getMessage(), e);
            throw new PayApiException("组装原始响应错误");
        }
    }

    private Map<String, String> assembleResponseHeader(Response okHttpResponse) {
        Map<String, String> responseHeaders = new ConcurrentHashMap<>();
        // use an OkHttp3.x compatible method
        int headerSize = okHttpResponse.headers().size();
        for (int i = 0; i < headerSize; ++i) {
            responseHeaders.put(okHttpResponse.headers().name(i), okHttpResponse.headers().value(i));
        }
        return responseHeaders;
    }
}
